vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 2

Tag 10

Erweiterte Möglichkeiten regulärer Ausdrücke

Gestern haben wir uns den Grundlagen der regulären Ausdrücke (regular expressions) gewidmet. Sie haben die wichtigsten Metazeichen kennengelernt und wissen jetzt, wie man sie verwendet, um Muster in Strings zu finden. Heute, im zweiten Teil unserer regex-Saga, bauen wir auf diesem Wissen auf und untersuchen etwas komplexere Möglichkeiten für den Einsatz regulärer Ausdrücke. In diesem Kapitel zeige ich Ihnen,

Übereinstimmungen extrahieren

Mit Hilfe von Mustern im Booleschen skalaren Kontext einer if- oder einer Schleifenbedingung können Sie feststellen, ob Ihr Muster mit einem Teil eines Strings übereinstimmt oder nicht. Auf diese Frage gibt es jedoch nur zwei Antworten: Ja oder Nein. Das mag zwar zur Überprüfung der Eingabe oder zum Ermitteln mehrerer Vorkommen eines Musters in einem String ganz nützlich sein, ist aber nur eine Seite der Medaille. Die Antworten Ja oder Nein sind zwar recht zweckmäßig, aber noch nützlicher ist es, wenn man herausfinden kann, welche Daten genau dem Muster entsprechen, um dann diese Daten später in dem Muster wiederzuverwenden oder eine Liste aller gefundenen Übereinstimmungen aufzubauen.

Ob das Vorkommen, das Sie mit einem Muster finden, nutzbringend ist oder nicht, hängt natürlich vom Muster ab. Lautet Ihr Muster /abc/, dann werden auch die gefundenen Daten abc lauten, was Ihnen natürlich bereits vorher bekannt war. Wenn jedoch Ihr Muster ungefähr so aussieht /\+.*/ (suche ein + dann eine beliebige Anzahl von Zeichen), kann Ihre Übereinstimmung aus einem beliebigen Satz von Zeichen bestehen, die zufällig nach einem +-Zeichen stehen. Dass man auf die gefundenen Vorkommen zugreifen kann, ist eine ganz wichtige Eigenschaft der regulären Ausdrücke.

In Perl gibt es mehrere Wege, um auf Fundstellen zuzugreifen, und verschiedene Möglichkeiten, diese - hat man sie erst einmal gefunden - zu nutzen. Gibt es eine Übereinstimmung, können Sie weiter hinten in dem gleichen Muster darauf Bezug nehmen, Sie können die Übereinstimmung in einer skalaren Variablen speichern oder eine Liste aller Übereinstimmungen anlegen. In diesem Abschnitt werden wir diese Möglichkeiten der Reihe nach besprechen.

Mit Klammern Rückbezüge herstellen

Gestern habe ich Ihnen gezeigt, wie Sie mit Klammern Teile des Musters zusammenfassen können oder wie sich damit die Prioritäten beim Mustervergleich ändern lassen. Der dritte und vielleicht wichtigste Einsatzbereich von Klammern ist, die gefundene Übereinstimmung zu speichern, um dann weiter hinten zum Aufbau eines komplexeren regulären Ausdrucks auf diese Übereinstimmung Bezug zu nehmen. Dieser Mechanismus, Übereinstimmungen zu sichern, wird in der Sprache der regulären Ausdrücke oft auch als Rückbezug bezeichnet.

Betrachten wir dazu folgendes Beispiel. Angenommen wir suchen Zeilen, die mit dem gleichen Wort beginnen und enden. Dabei ist es unerheblich, welches Wort das ist, solange es am Anfang und am Ende der Zeile gleich lautet. Sie könnten dazu zwei Mustertests, eine Schleife und mehrere if-Anweisungen aufsetzen. Besser wäre es jedoch, zu Beginn der Zeile das erste Wort abzufragen, den Wert zu speichern und dann zu schauen, ob dieses Wort auch am Ende der Zeile vorkommt. Und so sähe ein derartiger Ausdruck aus:

/^(\S+)\s.*\1$/

Ich möchte Ihnen diesen Ausdruck Zeichen für Zeichen aufschlüsseln. Das erste Zeichen ist ein Caret (^) und bezieht sich auf den Anfang der Zeile. Direkt daran schließt sich eine Klammer an, die ein Muster einschließt, das für später gespeichert werden soll. \S ist ein beliebiges Nicht-Whitespace-Zeichen, und \S+ bedeutet ein oder mehrere beliebige Nicht-Whitespace-Zeichen. Die schließende Klammer beendet den Teil, der gespeichert werden soll. Können Sie mir noch folgen? Das Muster innerhalb der Klammer schaut am Anfang der Zeile nach einer Reihe von Zeichen, gefolgt von einem Whitespace-Zeichen (Leerzeichen, Tabulator etc.).

Doch fahren wir fort. Anschließend haben wir ein einziges Whitespace-Zeichen (\s), kein oder mehrere Zeichen beliebiger Art (.*), \1 und dann das Zeichen für Ende der Zeile ($). Doch wozu der Ausdruck \1? Dieser Code ist eine Referenz auf das, was wir als Übereinstimmung in der Klammer gefunden haben. \1 besagt: »Setze das, was du im ersten Klammerpaar gefunden hast, hierher.« Damit wird genau das, was wir innerhalb dieser Klammer gefunden haben, später in dem Muster selbst erscheinen. Damit das gesamte Muster wahr zurückliefert, muss die referenzierte Übereinstimmung also auch am Ende der Zeile erscheinen. Alle Teile des Musters müssen gefunden werden, nicht nur der Klammerinhalt.

Angenommen, Sie haben folgende Zeile:

»Perl ist zur schnellen Skripterstellung am besten geeignet.
Wenn Sie Ihre Arbeit gut machen wollen, wählen Sie Perl.«

(Gut, ich gebe zu, das sind zwei Zeilen. Nehmen Sie jedoch einfach an, es wäre eine einzige Zeile innerhalb der Variablen $_). Wird das obige Muster, angewendet auf diese Zeile, eine Übereinstimmung (gleiches Wort am Anfang und am Ende der Zeile) finden? Schauen wir mal. Das Muster sucht zuerst den Zeilenanfang, dann alle Nicht- Whitespace-Zeichen bis zum ersten einfachen Whitespace-Zeichen. Das Wort »Perl« und das anschließende Leerzeichen stimmen mit dem Muster überein. (Beachten Sie, dass das Whitespace-Zeichen außerhalb der Klammer steht und somit nicht mitgespeichert wird.) Der nächste Teil des Musters (.*) schluckt alle Zeichen bis zum Ende der Zeile. Hiernach halten wir Ausschau nach einem Leerzeichen und einem weiteren Vorkommen der zuvor gefundenen Übereinstimmung, die wir am Anfang der Zeile gespeichert haben. Denken Sie daran, hinter \1 verbirgt sich die am Anfang gefundene Übereinstimmung, so dass wir nach dem Wort Perl suchen. Nachdem wir Perl gefunden haben, erwartet das Muster mit $ noch das Ende der Zeile - aber nein, was ist das? Die Zeile ist ja noch gar nicht zu Ende! Da steht ja noch ein Punkt! Und wegen diesem Punkt liefert das gesamte Muster falsch zurück.

Wir können das jedoch problemlos beheben. Durch folgende Änderung am Muster kann man die Interpunktion berücksichtigen:

/^(\S+)\s.*\s\1[.!?"]$/

Am wichtigsten ist jedoch dabei die Tatsache, dass sich \1 auf das bezieht, was durch das in Klammern stehende Muster gefunden wurde. Das ganze Muster ist nur erfolgreich, wenn der ermittelte Klammerinhalt noch einmal dort im String auftaucht, wo es durch die Referenz \1 vorgegeben wird.

Vielleicht wundern Sie sich, warum es gerade \1 heißt. Die Bezeichnung \1 resultiert daraus, dass es nur eine Klammer gibt, in der ein Muster gespeichert wird. Möglich sind durchaus mehrere gespeicherte Übereinstimmungen in einem Muster, die dann jeweils von Klammern eingeschlossen sind und mit den Zahlen \1, \2, \3 und so weiter angesprochen werden (gezählt wird von links nach rechts). Die Zahlen werden jeweils den öffnenden Klammern zugewiesen - was bedeutet, dass Sie eine Klammer nicht schließen müssen, bevor Sie eine neue öffnen. Sie können somit die Muster, für die Sie eine Übereinstimmung suchen, verschachteln und mit der Zahl dann auf die jeweilige Übereinstimmung Bezug nehmen.

Seien Sie jedoch vorsichtig mit dem Einsatz von Klammern - unabhängig davon, ob Sie damit Gruppen definieren, um die Prioritäten zu ändern, oder Übereinstimmungen sichern wollen - Perl wird auf alle Fälle die Werte speichern. Sie können dies verhindern, indem Sie eine besondere Form der Klammer verwenden: (?:muster) anstelle von (muster). Mehr dazu erfahren Sie im Abschnitt »Vertiefung« zu diesem Kapitel.

Übereinstimmungen in Variablen speichern

Rückbezüge ermöglichen es Ihnen, eine für einen Teil des Musters gefundene Übereinstimmung im Muster selbst wieder zu referenzieren. Sie können auf den bereits gefundenen Teil eines Musters, aber auch von außerhalb des Musters zugreifen. Zusätzlich zu der Numerierung der Rückbezüge durch \1, \2 und so weiter weist Perl den Werten der Teilfundstellen Skalarvariablen $1, $2 und so weiter zu. Dieser Mechanismus erweist sich als besonders praktisch, wenn man Teile aus einem String oder anderen Daten extrahieren möchte. Sehen Sie dazu folgendes Beispiel, bei dem das erste Wort aus einem String herausgelöst wird:

if (/^(\S+)\s/) {
print "Erstes Wort: $1\n";
}

Hierbei wird Perl, wenn es eine Entsprechung zu dem Muster findet (das heißt, wenn es einen Zeilenanfang gefolgt von einem oder mehreren Nicht-Whitespace-Zeichen und einem Whitespace-Zeichen findet), »Erstes Wort:« plus der gefundenen Übereinstimmung ausgeben. Wenn die Daten kein erstes Wort aufweisen (zum Beispiel wenn es sich um einen leeren String handelt oder es kein Whitespace- Zeichen in dem String gibt), wird nichts ausgegeben, da der Test falsch zurückliefert. Wie bereits erwähnt, muss das ganze Muster und nicht nur der Teil in Klammern stimmen, damit wahr zurückgeliefert werden kann.

Auf eines möchte ich Sie im Zusammenhang mit den Übereinstimmungsvariablen noch hinweisen: Die Werte dieser Variablen sind lokal zu ihrem Block, können nur gelesen werden und sind sehr kurzlebig. Schon beim nächsten Versuch, eine Übereinstimmung zu finden, verschwindet ihr alter Wert. Die Werte verschwinden auch, wenn ein Block endet. Mit anderen Worten, wenn Sie die Werte dieser Variablen ändern wollen, müssen Sie sie zur Sicherung in einer anderen Variablen oder in einer Liste ablegen. Übereinstimmungsvariablen dienen nur der vorübergehenden Speicherung.

Übereinstimmungen und Kontext

Bis jetzt sind uns Muster nur in einem skalaren Kontext und dort vornehmlich in booleschen Tests begegnet. Für die Verwendung von Mustern in einem skalaren booleschen Kontext gibt es zwei Regeln:

In beiden Fälle füllen Klammern innerhalb des Musters die Übereinstimmungsvariablen $1, $2, $3 und so weiter, und Sie können diese Werte dann innerhalb des Bedingungs- oder Schleifenblocks oder bis zum nächsten Mustervergleich verwenden.

Für Muster in einem Listenkontext gelten allerdings andere Regeln (Überraschung, Überraschung):

Da Muster in einem Listenkontext die gefundenen Übereinstimmungen als Liste zurückgeben, könnten Sie die Muster nutzen, um Ihre Daten in Elemente aufzusplitten. Hier eine Möglichkeit, einen Namen in die beiden Bestandteile Vor- und Nachname zu zerlegen:

($fn, $ln) = /^(\w+)\s+(\S+)$/

Eine Bemerkung zur Gier

In Kapitel 2, »Mit Strings und Zahlen arbeiten«, hatten wir eine kurze Diskussion über die Wahrheit, und jetzt werden wir über Gier sprechen. Vielleicht kommen wir weiter hinten im Buch auch noch auf Gerechtigkeit und Neid zu sprechen.

Doch Humor beiseite, eine heikles Problem beim Extrahieren von Mustern betrifft die Art und Weise, wie sich die quantifizierenden Metazeichen verhalten. Die Metazeichen +, *, ? und {} werden auch gierige Metazeichen genannt, da sie im Falle einer Übereinstimmung so viele Zeichen wie möglich in die Übereinstimmung mit aufnehmen bis hin zum letzten Zeichen in der Zeile.

Normalerweise verhalten sich Muster so, dass sie, wenn sie gefundene Übereinstimmungen zurückliefern sollen (das heißt, wenn das Muster in einem Listenkontext verwendet wird), die erste Übereinstimmung zurückliefern. Betrachten wir beispielsweise folgenden Ausdruck:

@x = /(\d\d\d)/;

Angenommen die Daten in $_ sehen folgendermaßen aus:

3443 32 784 2344 123 78932

Das Array @x endet als eine Liste von einem Element, den ersten drei Ziffern, in diesem Fall 344. Das Muster bricht immer nach dem ersten positiven Vergleich ab.

Die Quantifizierer *, ? und {} unterliegen allerdings anderen Regeln. Nehmen wir als Beispiel die Daten von oben und versuchen wir eine Übereinstimmung für folgendes Muster zu finden:

/(\d*)/;

Da * als »keines oder mehrere der vorhergehenden Zeichen« definiert ist, könnten Sie davon ausgehen, dass der Vergleich mit dem Muster abgebrochen wird, sobald diese Bedingung erfüllt ist, das heißt, sobald das erste passende Zeichen eingelesen wurde - was in unserem Zahlenbeispiel von oben die Zahl 3 zurückliefern würde. Die Ziffer entspricht zwar dem Muster, aber man darf nicht vergessen, dass * ein gieriger Quantifizierer ist, der bis an die Grenze des Möglichen alle Zeichen aufsaugt. Das Ergebnis eines Vergleichs dieses Musters mit dem Zahlenstring lautet daher 3443. Der *-Quantifizierer geht Zahl um Zahl vor, bis er auf ein Leerzeichen trifft. Ein Leerzeichen ist keine Zahl, und deshalb muss der Quantifizierer hier stoppen.

Hier sehen Sie ein noch schwierigeres Beispiel:

/'(.*)'/

Auf den ersten Blick scheint dieses Muster nach Zeichen in Anführungszeichen zu suchen (und $1 mit diesen Zeichen zu füllen). Versuchen Sie jedoch einmal, dieses Muster auf folgenden String anzuwenden:

"Sie sagte, 'Ich möchte diesen Käfer nicht essen,' und dann schlug sie mich."

Da die Sequenz .* gierig ist, vergleicht sie alle Zeichen zwischen den einfachen Anführungszeichen, liefert sie zurück (Ich möchte diesen Käfer nicht essen) und fährt dann damit fort, die restlichen Zeichen bis zum Ende der Zeile zu vergleichen. Da die Sequenz dabei das Anführungszeichen nicht verglichen hat, geht Perl rückwärts und versucht verschiedene Zeichen, bis es auf das Anführungszeichen stößt. Damit erhalten Sie zwar das erwartete Ergebnis, doch Perl wird viel Zeit aufwenden müssen, um zu dem Ergebnis zu gelangen. Es kann sogar noch schlimmer werden. Wenn Sie nämlich mehrere Anführungszeichen im String haben, wird Perl das erste akzeptieren, auf das es beim Rückwärtsgehen stößt. Nehmen wir folgenden String als Beispiel:

"'Ich verachte dich,' sagte sie und warf eine Kanne nach mir. 'Ich wünschte du wärest tot.'"

Wenn Sie versuchen, das gleiche Muster auf diesen String anzuwenden, wird $1 letztlich folgenden String enthalten:

Ich verachte dich,' sagte sie und warf eine Kanne nach mir. 'Ich wünschte du wärest tot.

Angenommen Sie wollten mit dem Muster ursprünglich nur nach dem Inhalt der ersten Anführungszeichen suchen (nur die Wörter Ich verachte dich), dann entspricht dieses Ergebnis absolut nicht Ihren Erwartungen.

Quantifizierer scheinen auf den ersten Blick eine clevere Lösung zum Füllen einer Lücke zwischen zwei Mustern zu sein. Aufgrund ihres gierigen Verhaltens sind sie dafür jedoch häufig absolut ungeeignet, und Sie werden nur frustriert werden, wenn Sie versuchen, sie dafür einzusetzen. Die bessere Lösung - sowohl um sicherzustellen, dass nur gefunden wird, wonach gesucht wurde, als auch um Perl davon abzuhalten, unnötige Zeit mit dem Rückwärtsdurchlaufen eines Strings zu verbringen - ist die Verwendung von negierten Zeichenklassen anstatt der Quantifizierer. Betrachten Sie das Problem einmal nicht als »alle Zeichen zwischen dem öffnenden und dem schließenden Anführungszeichen«, sondern als »ein öffnendes Anführungszeichen, dann einige Zeichen, die kein Anführungszeichen sind, und dann ein schließendes Anführungszeichen«.

Wenn Sie diesen Gedanken in ein Muster umsetzen, sieht das folgendermaßen aus:

/"([^"]+)"/

Das sind zwar einige Zeichen mehr, und es ist insgesamt etwas schwieriger zu lesen, aber dafür liefert das Muster auch garantiert die Zeichen zwischen den Anführungszeichen zurück und frißt nicht gierig alle Zeichen nach dem schließenden Anführungszeichen, die dann ein Rückwärtssuchen erforderliche machen. Merken Sie sich diese Regel: Wenn Sie ein Muster zwischen Begrenzern vergleichen wollen, verwenden Sie am besten eine negierte Zeichenklasse mit dem schließenden Begrenzungszeichen innerhalb der ekkigen Klammern.

Ein zweiter, weniger effizienter Weg, das gierige Verhalten der Metazeichen +, *, ? und {} zu unterdrücken, besteht darin, besondere, nichtgierige (»faule«) Versionen dieser Metazeichen zu verwenden: +?, *?, ?? und {}?. Mehr dazu im Abschnitt »Vertiefung«.

Muster für Suchen&Ersetzen-Operationen

Reguläre Ausdrücke lassen sich nicht nur sehr gut zum Suchen eines Musters oder zum Ablegen von Übereinstimmungen in Listen oder Variablen oder ähnlichem nutzen, sondern vor allem auch für das Ersetzen der Vorkommen des Musters durch einen anderen String. Dies entspricht den Suchen&Ersetzen-Operationen Ihres bevorzugten Textverarbeitungssystems oder Editors plus der geballten Leistungsfähigkeit und Flexibilität der regulären Ausdrücke.

Die Syntax, mit der Sie nach etwas suchen, um es dann mit einem anderen Muster zu ersetzen, lautet:

s/muster/ersetzung/

In dieser Syntax ist muster ein regulärer Ausdruck und ersetzung der String, durch den die gefundenen Vorkommen zu ersetzen sind. Fehlt die Angabe des Strings, durch den ersetzt werden soll, werden die gefundenen Übereinstimmungen aus dem Gesamtstring gelöscht. Ein Beispiel:

S/\s+/ /   # ersetze ein oder mehrere Whitespaces durch ein Leerzeichen

Wie bei den normalen Mustern sucht und ersetzt diese Syntax standardmäßig in $_. Verwenden Sie den Operator =~, wenn Sie in einem anderen String suchen wollen.

Die Suchen&Ersetzen-Syntax ersetzt nur die erste Übereinstimmung und liefert dann 1 zurück. Wird am Ende die Option /g (steht für global) angegeben, ersetzt Perl alle Vorkommen des Musters in dem String:

s/--/[md]/g  # ersetze zwei Bindestriche durch den Gedankenstrich [md]

Sie können, wie bei normalen Mustern, am Ende auch die Option /i verwenden, mit der die Suche unabhängig von der Groß- und Kleinschreibung durchgeführt wird (Vorsicht! Das bedeutet jedoch nicht, dass die Ersetzung sich dabei jeweils der Schreibweise anpaßt):

s/a/b/gi;   # ersetze [Aa] durch b, global

Lassen Sie sich nicht davon abhalten, innerhalb der Suchen&Ersetzen-Muster Klammern oder Übereinstimmungsvariablen zu verwenden. Man kann sie in vielfältiger Weise sinnvoll einsetzen:

s/^(\S+\b)/=$1=/g  # setzt =-Zeichen um das erste Wort
s/^(\S+)(\s.*)(\S+)$/$3$2$1/ # tauscht das erste mit dem letzten Wort

Anhänger von sed werden vermutlich die Verwendung von /1 und /2 und so weiter in dem Ersetzungsteil der Suchen&Ersetzen-Operationen befürworten. Doch auch wenn dies in Perl funktioniert (vor allem weil Perl Ihnen diese Referenzen durch Variablen ersetzt), sollten Sie sich deren Verwendung allmählich abgewöhnen. Offiziell ist der Ersetzungsteil in dem Ausdruck s/// ein ganz normaler String in doppelten Anführungszeichen, und \1 bedeutet in diesem Kontext an sich etwas anderes.

Mehr zu split

Erinnern Sie sich noch an die split-Funktion aus Kapitel 5, »Mit Hashes arbeiten«? Wir haben mit split die Vor- und Nachnamen in getrennten Listen ausgeben lassen:

($fn, $ln) = split(" ", $in);

Damals habe ich Ihnen erklärt, dass die Verwendung von split mit einem Leerzeichen in Anführungszeichen ein Sonderfall sei, der sich nur auf Daten anwenden lasse, in denen die Felder durch Whitespace-Zeichen getrennt sind. Um split für Daten zu verwenden, die durch irgendein anderes Zeichen getrennt sind oder die einer komplizierteren Verarbeitung bedürfen, um die Elemente zu finden, müssen Sie split um einen regulären Ausdruck für das zu vergleichende Muster ergänzen:

($fn, $ln) = split(/\s+/, $in);                    # bei Whitespace zerlegen 
@nums = split(//, $num);                    # zerlege 123 in (1,2,3)
@fields = split(/\s*,\s*/, $in);                    # zerlege durch Kommata getrennte Felder,
                    # mit oder ohne Whitespace um das Komma

Das erste Beispiel, das den String dort aufteilt, wo ein oder mehrere Leerzeichen auftauchen, entspricht im Verhalten zum einem unserem Beispiel aus Kapitel 5 mit dem Leerzeichen in Anführungszeichen und zum anderen einer Version, in der split ohne irgendein Muster verwendet wird (die Syntax mit dem Leerzeichen in Anführungszeichen lehnt sich an das Unix-Tool awk an, das Strings auf gleiche Art und Weise in Teilstrings zerlegt).

Sie können split als drittes Argument auch eine Zahl mitgeben, die angibt, in wie viele Teile die Daten zerlegt werden sollen:

($ln, $fn, $daten{$ln}) = split(/,/ $in, 3);

Dieser reguläre Ausdruck wäre beispielsweise für folgende Daten sehr nützlich:

Jones,Tom,braun,blau,64,32

Der split-Befehl zerlegt die Daten beim Komma in drei Elemente: den Nachnamen, den Vornamen und den Rest. Die Zuweisung wird den Nachnamen und den Vornamen jeweils in Skalarvariablen ablegen und »den Rest« in einem Hash, das den Nachnamen als Schlüssel verwendet.

Normalerweise enthalten die Teilstrings, die in der endgültigen Liste gespeichert werden, nichts, was dem Suchmuster entspricht. Wenn Sie jedoch im Muster Klammern verwenden, wird alles, was innerhalb dieser Klammern dem Suchmuster entspricht, auch in der endgültigen Liste erscheinen, wobei jede Entsprechung ein eigenes Listenelement darstellt. Angenommen Sie hätten folgenden String:

1:34:96:54:0

Wenn Sie diesen String an den Doppelpunkten zerlegen (mit dem Muster /:/), erhalten Sie eine Liste aller Elemente, die keine Doppelpunkte sind, also eine Liste aller Zahlen. Mit dem Muster /(:)/ erhalten Sie folgende Liste, die sowohl Teilstrings enthält, die nicht Bestandteil des Musters sind, als auch solche, die dem Klammerinhalt des Musters entsprechen:

1, ':', 34, ':', 96, ':', 54, ':', 0

split zusammen mit normalem Pattern Matching sollte es Ihnen ermöglichen, Ihre Strings auf fast jede beliebige Weise zu zerlegen. Greifen Sie mit Mustern und Rückbezügen auf die gewünschten Teile zu, und zerlegen Sie den String dann mit split und unter Angabe der Teile, die Sie nicht benötigen, in seine Bestandteile. Jeder, der die Suchmuster korrekt einsetzt und das Format der Eingabedaten versteht, wird keine Schwierigkeiten haben, Daten fast jeden Formats mit nur wenigen Codezeilen zu verarbeiten. In einer Sprache wie C wäre dies nur mit einem wesentlich größeren Aufwand zu bewerkstelligen.

Pattern Matching über mehrere Zeilen

Bis jetzt sind wir beim Pattern Matching von der Annahme ausgegangen, dass sich der Vergleich nur auf einzelne Zeilen (Strings) beschränkt, die aus einer Datei oder von der Tastatur aus eingelesen werden. Diese Annahme besagt, dass der String, der durchsucht wird, keine Zeichen für Zeilenvorschub oder Wagenrücklauf enthält, und dass die Anker für Anfang und Ende der Zeile sich auf den Anfang und das Ende des Strings selbst beziehen. Für den while-Code (<>), den wir bisher geschrieben haben, ist diese Annahme ziemlich sinnvoll.

Oft jedoch wollen Sie ein Muster über mehrere Zeilen hinweg vergleichen, vor allem, wenn Ihre Eingabedaten aus Sätzen oder Absätzen bestehen, deren Zeilengrenzen von der jeweiligen Textformatierung abhängen. Wenn Sie zum Beispiel alle Vorkommen der Bezeichnung »Exegetic Frobulator 5000« auf einer Webseite suchen, wollen Sie nicht nur die Bezeichnungen finden, die auf einer logischen Zeile stehen, sondern auch die, die sich über die Zeilengrenze hinweg in die nächste Zeile erstrecken.

Dazu müssen Sie in zwei Schritten vorgehen. Zuerst müssen Sie Ihre Eingaberoutinen dahingehend ändern, dass die gesamte Eingabe als ein String eingelesen und nicht Zeile für Zeile verarbeitet wird. Damit erhalten Sie einen extrem langen String mit Zeichen für »Neue Zeile« und »Wagenrücklauf«. Zweitens müssen Sie je nach Suchmuster, das Sie verwenden, Perl mitteilen, dass die Neue-Zeile-Zeichen anders zu handhaben sind.

Mehrere Zeilen der Eingabe speichern

Es gibt mehrere Möglichkeiten, Ihre gesamte Eingabe als einen String einzulesen. In einem Listenkontext könnten Sie <> verwenden:

@input = <>;

Die obige Zeile birgt allerdings ein großes Gefahrenpotential. Wenn nämlich Ihre Eingabe sehr, sehr groß ist, könnte der gesamte zur Verfügung stehende Speicher Ihres Systems in dem Versuch, all diese Daten einzulesen, aufgebraucht werden. Und es gibt keine Möglichkeit, mittendrin abzubrechen. Etwas weniger aggressiv ist der Ansatz, die vornehmlich in Absätzen vorliegenden Daten mit Hilfe der Sondervariablen $/ einzulesen. Wenn Sie $/ auf einen Null-String setzen ($/ = "";), dann liest Perl einen Absatz einschließlich der Neue-Zeile-Zeichen ein und hört auf, sobald es auf zwei oder mehr Neue-Zeile-Zeichen in einer Reihe trifft (dabei gehen wir davon aus, dass Ihre Eingabedaten eine oder mehrere Leerzeilen zwischen den Absätzen enthalten):

$/ = "";
while (<>) { # liest einen Absatz keine, Zeile
# $_ enthält den gesamten Absatz und nicht nur eine Zeile
}

Der dritte Weg, mehrere Zeilen in einen einzigen String einzulesen, besteht darin, verschachtelte while-Schleifen zu verwenden und die eingelesenen Zeilen an einen Eingabestring anzuhängen, bis ein bestimmter Begrenzer erreicht ist. In streng codierten HTML-Dateien zum Beispiel endet ein Absatz mit einem </P>-Tag, so dass Sie in diesen Dateien die Eingabedaten bis zu diesem Punkt einlesen könnten:

while (<>) {
if (/(.*)<\/P>/) {
$in.=$1
} else {
$in.=$_
}
}

Eingabedaten mit Neue-Zeile-Zeichen

Haben Sie erst einmal Ihre mehrzeiligen Eingabedaten in einem String untergebracht und entweder in $_ oder einer anderen Skalarvariablen gespeichert, können Sie damit beginnen, diese Daten nach Mustern zu durchsuchen, die sich über mehrere Zeilen erstrecken. Einiges gilt es jedoch zu beachten, wenn Sie eine Mustersuche bei Daten mit eingebetteten Neue-Zeile-Zeichen durchführen wollen:

Der letzte Punkt hierbei ist am problematischsten. Betrachten wir folgendes Muster, das den Quantifizierer .* verwendet, um nach einer anfänglichen »Von: «-Überschrift den Rest einer Zeile auszulesen:

/Von: (.*)/

Dieses Muster sucht zuerst nach den Zeichen Von: und füllt dann $1 mit dem Rest der Zeile. Dies bereitet in der Regel bei einem String, der am Ende einer Zeile aufhört, auch keinerlei Probleme. Wenn der String jedoch mehrere Zeilen lang ist, wird eine Übereinstimmung nur bis zum ersten Neue-Zeile-Zeichen (\n) erzielt, denn das Punktzeichen berücksichtigt in der Regel die Zeichen für Neue Zeile.

Sie könnten das Problem umgehen, indem Sie das Muster ändern, so dass es nach einem oder mehreren Wörtern oder Whitespace-Zeichen sucht, und den Punkt auf diese Weise ganz vermeiden. Das wäre jedoch sehr arbeitsaufwendig. Was Sie hier benötigen, ist die Option /s am Ende Ihres Musters, die Perl anweist, das Metazeichen Punkt um das Neue-Zeile-Zeichen (\n) zu ergänzen. Die Option /s hat keinen Einfluß auf das Verhalten des restlichen Mustervergleichs - ^ und $ repräsentieren weiterhin die Zeichen für Anfang und Ende des Strings.

Wenn Sie einen regulären Ausdruck mit den Zeichen ^ und $ verwenden, werden Sie mehrzeilige Strings unter Umständen anders behandeln wollen als einzeilige Strings. Standardmäßig beziehen sich ^ und $ auf den Anfang und das Ende des Strings, wobei die Zeichen für eine neue Zeile ignoriert werden. Wenn Sie jedoch die Option /m verwenden, bezieht sich ^ nicht nur auf den Anfang der des Strings, sondern auch auf den Anfang einer Zeile (die Position direkt nach einem \n), und $ betrifft sowohl das Ende des Strings als auch das Ende der Zeile (die Position direkt vor dem \n). Mit anderen Worten, wenn Ihr String vier Textzeilen enthält, wird das Zeichen ^ viermal zu einer Übereinstimmung führen. Das gleiche gilt für $. Dazu ein Beispiel:

while (/^(\w)/mg) {
print "$1\n";
}

Diese while-Schleife gibt das erste Wort jeder Zeile in $_ aus, unabhängig davon, ob die Eingabe eine Zeile oder mehrere enthält.

Wenn Sie jedoch die Option /m verwenden und ausnahmsweise den Anfang oder das Ende des Strings abfragen wollen, lassen sich ^ und $ dafür nicht länger verwenden. Aber keine Angst, auch dafür hat Perl eine Lösung parat: Mit \A und \Z beziehen Sie sich auf den Anfang und das Ende des Strings, unabhängig vom Status von /m.

Sie können problemlos die Optionen /s und /m zusammen verwenden. Sie müssen sich lediglich merken, dass /s das Verhalten des Punktes beeinflußt und /m das Verhalten von ^ und $. Darüber hinaus stellen eingebettete Neue-Zeile-Zeichen in Strings kein Problem für Pattern Matching dar.

Eine Zusammenfassung der Optionen und Escape- Zeichen

Im Verlaufe dieses Kapitels habe ich Ihnen diverse Optionen vorgestellt, die Sie zusammen mit den Mustern verwenden können, sowie eine Reihe von besonderen Escape-Zeichen, die innerhalb von Mustern verwendet werden können.

In Tabelle 10.1 finden Sie die Optionen, die Sie an das Ende eines Musters (m// oder nur //) setzen können, aber auch jene, die zusammen mit dem Ersetzungsausdruck gültig sind (s///).

Ich habe in diesem Kapitel nicht alle der hier aufgeführten Optionen besprochen. Einige werden Ihnen noch im Abschnitt »Vertiefung« vorgestellt. Falls Sie selbst nachforschen wollen, welche Möglichkeiten Ihnen bestimmte Optionen im Detail bieten, sollten Sie dazu in die perlre-Manpage schauen.

Option

Verwendung

g

sucht alle Vorkommen (nicht nur eines)

i

sucht nach Groß- und Kleinbuchstaben

m

verwendet ^ und $ für Neue Zeile

o

interpoliert das Muster einmal (steigert die Effizienz)

s

(.) Punkt schließt die Neue-Zeile-Zeichen mit ein

x

erweitert die regulären Ausdrücke (kann Kommentare und Whitespace-Zeichen mit einschließen)

e

bewertet Ersetzung als Perl-Ausdruck (nur Substitution mit s///)

Tabelle 10.1: Optionen zum Pattern Matching und Suchen&Ersetzen  

Tabelle 10.2 enthält die besonderen Escape-Zeichen, die in den regulären Ausdrücken zusätzlich zu den normalen String-Escape-Zeichen (\t, \n, Backslash, der Metazeichen als gewöhnliches Zeichen interpretiert) verwendet werden können.

Escape-Zeichen

Verwendung

\A

Anfang eines Strings

\Z

Ende eines Strings

\w

Wortzeichen

\W

Nichtwortzeichen

\b

Wortgrenze

\B

Nichtwortgrenze

\s

Whitespace-Zeichen

\S

Nicht-Whitespace-Zeichen

\d

Ziffer

\D

Nichtziffer

\Q

Alle Sonderzeichen mit einem Escape-Zeichen versehen

\E

Beendet eine \Q-Sequenz

Tabelle 10.2: Escape-Zeichen für Pattern Matching

Ein Beispiel: Der Grafik-Extraktor

Beenden möchte ich meine bisherigen Ausführungen mit einem Beispiel für einen ziemlich umfangreichen regulären Ausdruck (eigentlich sind es zwei Ausdrücke) innerhalb eines Perl-Skripts. Dieses Skript erhält als Eingabe eine HTML-Datei, geht die Datei durch und sucht nach eingebetteten Grafiken (mit Hilfe des <IMG>-Tags in HTML). Danach gibt es eine Liste der Grafiken in der Seite aus sowie eine Liste der verschiedenen Attribute zu der jeweiligen Grafik (ihre Position, Breite oder Höhe, Textalternative etc.). Die Ausgabe des Skripts wird in etwa so aussehen:

---------------
Grafik: title.gif
HSPACE: 4
VPSACE: 4
ALT: *
---------------
Grafik: smbullet.gif
ALT: *
---------------
Grafik: rib_bar_wh.gif
BORDER: 0
HSPACE: 4
WIDTH; 50
HEIGHT: 50
ALT: --

Falls Sie mit HTML nicht vertraut sind, sehen Sie hier ein Beispiel für das <IMG>-Tag, das an einer beliebigen Stelle in einer HTML-Datei eingefügt werden kann:

<IMG SRC="imgfile.gif" WIDTH=50 HEIGHT=75 ALT="Pinguine">

Dieses Tag weist einige knifflige Besonderheiten auf, die Ihre Aufgabe schwieriger machen, als sie auf den ersten Blick scheint. Zum einen kann das Tag selbst in Groß- oder in Kleinbuchstaben vorkommen, und es kann sich über mehr als eine Zeile erstrecken. Die Attribute (die Schlüssel/Werte-Paare nach dem IMG-Teil) können ebenfalls groß oder klein geschrieben sein, können Leerzeichen links und rechts des Gleichheitszeichen haben und in Anführungszeichen stehen oder nicht. Die Werte können Leerzeichen enthalten (müssen dann allerdings von Anführungszeichen umschlossen sein). Es ist nur ein Attribut erforderlich, das SRC-Attribut, aber es gibt weitere Attribute, die in beliebiger Reihenfolge erscheinen können.

All diese Möglichkeiten sind dafür verantwortlich, dass der reguläre Ausdruck wesentlich komplexer wird, als wenn damit nur der Inhalt zwischen dem öffnenden und dem schließenden Tag aufgenommen werden sollte. Um genau zu sein, habe ich in diesem Skript die Aufgabe auf zwei reguläre Ausdrücke verteilt: Einer sucht das IMG- Tag in der Datei und zieht es heraus, und ein anderer extrahiert und parst die einzelnen Attribute.

Listing 10.1 enthält den Code zu diesem Skript. Versuchen Sie schon einmal, es durchzugehen, um ein Gefühl für den Aufbau zu bekommen. Aber sorgen Sie sich nicht zu sehr, wenn Sie die Muster noch nicht vollständig verstehen.

Listing 10.1: Das Skript img.pl

1:  #!/usr/bin/perl -w
2:
3: $/ = ""; # Absatzeingabe-Modus
4: $raw = ""; # rohe Attribute
5: %atts = (); # Attribute
6:
7: while (<>) {
8: while (/<IMG\s+([^>]+)>/ig ) {
9: $raw = $1;
10: while ($raw =~ /([^ =]+)\s*=\s*("([^"]+)"|[^\s]+\s*)/ig) {
11: if (defined $3) {
12: $atts{ uc($1) } = $3;
13: } else { $atts{ uc($1)} = $2; }
14: }
15: if ($raw =~ /ISMAP/i) {
16: $atts{'ISMAP'}= "Yes";
17: }
18:
19: print '-' x 15;
20: print "\nGrafik: $atts{'SRC'}\n";
21: foreach $key ("WIDTH", "HEIGHT",
22: "BORDER", "VSPACE", "HSPACE",
23: "ALIGN", "ALT", "LOWSRC", "ISMAP") {
24: if (exists($atts{$key})) {
25: $atts{$key} =~ s/[\s]*\n/ /g;
26: print " $key: $atts{$key}\n";
27: }
28: }
29: %atts = ();
30: }
31: }

Dieses Skript besteht aus zwei Teilen: einem Abschnitt, der die Daten aus der Eingabe herauszieht, und einem Abschnitt, der in Protokollform ausgibt, was gefunden wurde.

Der erste Teil verwendet eine Reihe von verschachtelten while-Schleifen, um die HTML-Datei durchzugehen: Zeile 7 durchläuft die gesamte Eingabe, Zeile 8 sucht in der Eingabe nach jedem Vorkommen des Image-Tags, und Zeile 10 durchläuft jedes Attribut im <IMG>-Tag und speichert es in einem Hash namens %atts mit dem Attributnamen als Schlüssel.

Nachdem der Hash %atts gefüllt ist, müssen wir nur noch die Werte ausgeben. Da ich möchte, dass sie in einer speziellen Reihenfolge ausgegeben werden, habe ich in Zeile 21 mit einer foreach-Schleife angegeben, in welcher Reihenfolge die Schlüssel auszugeben sind.

Unser Hauptaugenmerk in diesem Skript soll aber auf den regulären Ausdrücken in den Zeilen 8 und 10 liegen. Betrachten wir also diese zwei Muster im Detail. Zeile 8 lautet:

while (/<IMG\s+([^>]+)>/ig) {

Gehen wir diesen regulären Ausdruck zeichenweise durch: Zuerst suchen wir nach den Zeichen <IMG (der Öffnungsteil des Tags) und dann nach einem oder mehreren Whitespace-Zeichen, gefolgt von einem oder mehreren Zeichen, die nicht > sind. Das Muster selbst wird mit dem eigentlichen >-Zeichen, das das Ende des Tags signalisiert, beendet.

Beachten Sie die Klammern um den Teil, der die »ein oder mehrere Zeichen, die nicht > sind« repräsentiert - genau dieser Teil interessiert uns besonders, da er die Attribute für die Grafik enthält. Dieser Teil des Musters wird herausgezogen und gespeichert, so dass er später in dem Rumpf der Schleife verwendet werden kann.

Eine Bemerkung wert sind auch die Optionen am Ende des Musters: /i steht für eine Suche, die Groß- und Kleinschreibung unberücksichtigt läßt (gesucht wird sowohl <img...> als auch <IMG...>, und /g steht für eine globale Suche (wir durchlaufen für jedes <IMG, auf das wir stoßen, einmal die while-Schleife innerhalb der Schleife). Beachten Sie, dass wir /s oder /m eigentlich nicht explizit angeben müssen, um die Suche über Zeilengrenzen hinweg auszuführen - HTML erfordert Zeilenumbrüche nur für Whitespace, und unser Muster verwendet /s für alle Whitespace-Zeichen, so dass wir in dieser Hinsicht auf der sicheren Seite sind. Wir werden kein Problem mit Tags haben, die sich über mehrere Zeilen erstrecken.

Im Rumpf der Schleife können wir die Attribute-Liste in der Variablen $raw (Zeile 9) speichern. Wir sind dazu gezwungen, da die Werte von $1, $2, $3 und so weiter alle nur kurzlebig sind - sie werden beim nächsten Mustervergleich zurückgesetzt. Damit kommen wir zu Zeile 10 und dem folgenden wirklich fast undurchschaubaren regulären Ausdruck:

while ($raw =~ /([^ =]+)\s*=\s*("([^"]+)"|[^\s]+\s*)/ig) {

Dieser reguläre Ausdruck läßt sich in vier wichtige Teile gliedern, die ich in Abbildung 10.1 veranschaulichen möchte:

Abbildung 10.1:  Die Teile des regulären Ausdrucks in img.pl

Die Teile lassen sich auch wie folgt beschreiben:

Beachten Sie die Position der Klammern für die Werte, und vergegenwärtigen Sie sich die Regel für Übereinstimmungsvariablen: Die Zahlen werden auf der Basis der öffnenden Klammer vergeben. Deshalb repräsentiert $2 den kompletten Wert, während $3 der Wert minus der Angabe in Anführungszeichen ist - falls der Wert überhaupt Anführungszeichen hatte.

In den Zeilen 11 bis 14 werden beide Fälle berücksichtigt, indem wir testen, ob $3 gesetzt ist. Wenn ja, hat unser Wert Anführungszeichen, und der Teil, der nicht in den Anführungszeichen steht, wird im Hash %atts gespeichert. Ist $3 nicht definiert, steht unser Wert nicht in Anführungszeichen, und wir können statt dessen $2 in %atts ablegen. Außerdem möchte ich Sie darauf hinweisen, dass wir die Funktion uc verwendet haben, um unsere Attributnamen in Großbuchstaben zu wandeln, bevor sie gespeichert werden.

Die Zeilen 15 bis 17 behandeln einen Sonderfall des <IMG>-Tags: Das Attribut ISMAP übernimmt keinen Wert. Es zeigt lediglich an, ob es sich bei der Grafik um eine Imagemap handelt. (Eine Imagemap ist eine anklickbare Grafik, das heißt, Sie können verschiedene Bereiche der Grafik anklicken und lösen damit unterschiedliche Aktionen aus.) Diese Art von anklickbarer Grafik wird in HTML normalerweise nicht mehr verwendet (das Tag wurde durch ein anderes Tag ersetzt), aber der Vollständigkeit halber habe ich es mit aufgenommen. Wir mußten dies zu einem Sonderfall machen, da es nicht durch den Ausdruck in Zeile 10 abgefangen wird.

Nach Ausführung all dieser Schleifen und Muster haben wir einen Hash vorliegen, in dem alle gefundenen Attribute des <IMG>-Tags gespeichert sind. Uns bleibt nur noch, diese Werte auszugeben. Die Schleife in Zeile 21 durchläuft alle möglichen Werte für das <IMG>-Tag in der Reihenfolge, in der sie ausgegeben werden. Jedes <IMG>, das wir in der Datei finden, kann potentiell eine Teilmenge dieser Attribute aufweisen, die bis auf SRC alle optional sind. Deshalb müssen wir in Zeile 24 einen Test durchführen, um sicherzustellen, dass das Attribut auch tatsächlich existiert, bevor es ausgegeben wird. Die Funktion exists testet einen Hash, um festzustellen, ob ein Schlüssel existiert, und liefert wahr oder falsch zurück.

Ungewöhnlich ist auch die Zeile 25, in der wir eine schnelle Suchen&Ersetzen- Operation auf dem Wert durchführen. Damit wollen wir die Fälle abfangen, in denen sich das Attribut auf mehrere Zeilen verteilt - der Wert also ein Neue-Zeile-Zeichen enthält, das in der endgültigen Tabelle nicht mit ausgegeben werden soll. Dieser kleine reguläre Ausdruck sucht optionale Whitespace-Zeichen gefolgt von einem Neue-Zeile- Zeichen und ersetzt sie durch ein einfaches Leerzeichen.

Zum Schluß in Zeile 29 löschen wir den Attribut-Hash für die nächste Runde und das nächste <IMG>-Tag.

Dieses Skript ist zwar kurz, zeigt aber vorbildlich, für welche Art Aufgaben sich Perl besonders gut eignet: komplizierte Muster in Texten zu suchen und diese dann in anspruchsvollen Protokollen auszugeben. Wenn Sie die gleiche Aufgabe in C lösen wollten, würden Sie mit Sicherheit mehr als 30 Zeilen dafür benötigen.

Tips zum Erstellen regulärer Ausdrücke

Je nach der Komplexität Ihrer Daten oder der Aufgabe, die damit zu bewerkstelligen ist, kann die Formulierung eines regulären Ausdrucks sehr einfach sein oder mehrerer Überarbeitungen bedürfen. Hier möchte ich Ihnen einige Tips geben, die Ihnen helfen sollen, Suchmuster zu verwenden:

Vertiefung

Reguläre Ausdrücke gehören zu den Themen, mit denen man ein ganzes Buch füllen und trotzdem noch Fragen offenlassen kann. In den Kapiteln von gestern und heute habe ich Ihnen die Grundlagen vermittelt, wie man reguläre Ausdrücke aufbaut und in eigenen Programmen verwendet. Es gibt jedoch noch viele Punkte, die ich nicht angesprochen habe, unter anderem eine Unmenge von Metazeichen und Perl- spezifischen regulären Ausdrücken. In diesem Abschnitt möchte ich Ihnen einen Überblick über einige davon geben.

Weitere Informationen zu anderen Aspekten der regulären Ausdrücke in Perl finden Sie in der perlre-Manpage, die ziemlich aufschlußreich ist. Wenn Sie feststellen, dass Ihnen die Arbeit mit regulären Ausdrücken großen Spaß macht und Sie des Englischen mächtig sind, sollten Sie die Anschaffung des Buches »Reguläre Ausdrücke« (Jeffrey Friedl, O'Reilly Verlag) in Erwägung ziehen. Dieses Buch beschreibt erstaunlich detailliert reguläre Ausdrücke aller Art - sowohl von Perl als auch von anderen Sprachen.

Weitere Metazeichen

Mit den Metazeichen, die ich gestern und heute beschrieben habe, habe ich Ihnen den größten Teil der elementaren Zeichen vorgestellt, die in den meisten regulären Ausdrücken (nicht nur bei Perl) Verwendung finden. Perl umfaßt eine Reihe von zusätzlichen Metazeichen, die noch andere Möglichkeiten bieten, komplexe Muster zu erstellen (oder die gefundenen Muster zu verarbeiten).

Als erstes möchte ich hier die nichtgierigen Versionen der Quantifizierer *, +, ? und {} nennen. Wie Sie in diesem Kapitel gelernt haben, gehören die Quantifizierer eigentlich zu den gierigen Metazeichen. Sie vergleichen alle Zeichen und damit weit mehr, als von Ihnen erwartet - was Ihnen manchmal zum Nachteil gereichen kann, wenn Sie herausfinden wollen, wonach das Muster eigentlich sucht. Zu diesen Quantifizierern bietet Perl Ihnen nun als Gegenstücke die nichtgierigen Quantifizierer (manchmal auch faule Quantifizierer genannt): *?, +?, ?? und {}?. Diese Quantifizierer vergleichen die Mindestanzahl der Zeichen, die für den Mustervergleich nötig sind, während die regulären Quantifizierer die maximale Zahl der Zeichen vergleicht. Dies kann in manchen Situationen nützlich sein; Sie sollten jedoch nicht vergessen, wenn möglich auch mit negierten Zeichenklassen zu arbeiten. Die faulen Quantifizierer sind nicht so effizient wie eine negierte Zeichenklasse und führen oft zu unerwarteten Ergebnissen.

Das Konstrukt (?:muster) ist eine Variante zu den Klammern, mit denen Muster zusammengefaßt und die Ergebnisse in den Übereinstimmungsvariablen $1, $2, $3 etc. gespeichert werden. Wenn Sie Klammern verwenden, um einen Ausdruck als Gruppe zusammenzufassen, wird das Ergebnis automatisch auch gesichert, ob Sie wollen oder nicht. Mit dem Konstrukt (?:muster) hingegen wird der Ausdruck als Einheit zusammengefaßt und ausgewertet, das Ergebnis jedoch nicht gespeichert. Dies ist effizienter als die Verwendung der normalen Klammern, wenn das Ergebnis keine Rolle spielt.

Mit dem Konstrukt (?o) können Sie die Optionen für das Pattern Matching innerhalb des Musters selbst verwenden. So können Sie damit zum Beispiel bestimmte Teile des Ausdrucks von der Unterscheidung der Groß- und Kleinschreibung befreien. Das o in dem Konstrukt kann eine beliebige gültige Option für den Mustervergleich sein.

Die Voraussschau ist eine Besonderheit der regulären Ausdrücke in Perl, die es Perl erlaubt, den String kurz zu überfliegen und zu schauen, ob es nicht irgendwo eine Übereinstimmung mit dem Muster gibt, ohne dass dabei die Position im String geändert oder irgend etwas den geklammerten Teilen des Musters hinzugefügt wird. Das entspricht in etwa der Aussage: »Wenn der nächste Teil dieses Musters X enthält, dann stimmt dieser Teil überein«, ohne dass dabei die Position verlassen wird. Mit (?=muster) erzeugen Sie ein positives Muster für die Vorausschau (wenn muster im weiteren Verlauf des Strings eine Übereinstimmung hat, dann hat der vorangegangene Teil des Musters auch eine Übereinstimmung). Das Gegenteil davon ist ein negatives Muster (?!muster). Es stimmt nur dann überein, wenn zu dem muster keine Übereinstimmung gefunden wird.

Besondere Variablen

Zusätzlich zu den Übereinstimmungsvariablen $1, $2 und so weiter gibt es in Perl noch die Variablen $', $& und , die einen Kontext für die Textfundstellen des Musters bereithalten. $' bezieht sich dabei auf den Text bis zur Fundstelle, $& steht für den gefundenen Text und für den Text nach der Fundstelle (beachten Sie den Apostroph, der nicht mit dem einfachen Anführungszeichen ' identisch ist). Im Gegensatz zu den kurzlebigen Übereinstimmungsvariablen halten diese Variablen ihre Werte bis zum nächsten erfolgreichen Vergleich fest - unabhängig davon, ob der ursprüngliche String sich geändert hat oder nicht. All diese Variablen zehren jedoch enorm an der Leistung, deshalb sollten Sie sie wenn möglich vermeiden.

Die Variable $+ gibt die höchste Zahl der definierten Übereinstimmungsvariablen an. Wurden zum Beispiel $1 und $2, jedoch nicht $3, mit Werten gefüllt, wird $+ auf 2 gesetzt.

Optionen

Im Verlaufe dieses Kapitels haben Sie den größten Teil der Optionen für reguläre Ausdrücke in Perl kennengelernt (sowohl m// als auch s///). Zwei sind jedoch noch unerwähnt geblieben: /x für erweiterte reguläre Ausdrücke und /o, um wiederholtes Kompilieren des gleichen regulären Ausdrucks zu vermeiden.

Mit der Option /x können Sie reguläre Ausdrücke zur besseren Lesbarkeit mit Whitespace-Zeichen und Kommentaren versehen. Wenn Sie normalerweise Leerzeichen in einem Muster verwenden, werden diese Leerzeichen als Teil des Musters selbst betrachtet. Mit der Option /x werden nicht nur alle Leerzeichen und Neue-Zeile-Zeichen ignoriert, es ist damit auch möglich, Kommentare zu den einzelnen Zeilen des regulären Ausdrucks einzubauen. So könnten wir zum Beispiel den regulären Ausdruck in unserem Skript img.pl, der folgendermaßen aussah:

while ($raw =~ /([^ =]+)\s*=\s*("([^"]+)"|[^\s]+\s*)/ig) {

auch wie folgt schreiben:

while ($raw =~ /([^ =]+)     # sucht den Attributnamen
\s*=\s* # sucht Gleichheitszeichen mit und ohne
# Whitespaces
("([^"]+)"| # sucht und extrahiert Werte in
# Anführungszeichen
[^\s]+\s*) # oder sucht Werte nicht in Anführungszeichen
/igx) {

Die Verwendung von erweiterten regulären Ausdrücken kann eine große Hilfe sein und die Lesbarkeit eines regulären Ausdrucks verbessern.

Schließlich gibt es noch die Option /o, die dazu dient, Perl beim Kompilieren und Einlesen eines regulären Ausdrucks, der über eine Skalarvariable interpoliert wurde, zu optimieren. Betrachten wir folgendes Codefragment:

while (<>) {
if (/$muster/) {
...
}
}

In diesem Fragment wird das in $muster gespeicherte Muster interpoliert und in ein richtiges Muster kompiliert, das Perl versteht. Das Problem dabei ist jedoch, dass dieses Muster sich innerhalb einer Schleife befindet und sich deshalb der Prozeß bei jedem Durchlauf der Schleife wiederholt. Mit der Angabe von /o am Ende des Musters teilen Sie Perl mit, dass sich das Muster nicht ändert und deshalb nur einmal kompiliert werden muss, um dann einfach wiederverwendet zu werden:

if (/$muster/o) {  # nur einmal kompilieren

Informationen zu diesen Metazeichen, Variablen und Optionen finden Sie auch in der Hilfsdokumentation perlre-Manpage.

Zusammenfassung

Im heutigen Kapitel haben wir, aufbauend auf den Grundlagen von gestern, das Thema der regulären Ausdrücke vertieft. Wir haben angesprochen, wie man Fundstellen aus einer Pattern-Matching-Operation mit Hilfe von Klammern herauszieht und wie man durch die Verwendung von Rückbezügen und Übereinstimmungsvariablen gefundene Vorkommen speichert, um später auf sie zuzugreifen.

Im Rahmen dieser Diskussion haben Sie Pattern Matching in verschiedenen Kontexten kennengelernt (skalare Kontexte liefern wahr oder falsch zurück, Listenkontexte liefern Listen der Übereinstimmungen zurück). Außerdem habe ich Ihnen etwas über das gierige Verhalten der quantifizierenden Metazeichen und die erweiterten Möglichkeiten der split-Funktion erzählt. Wenn Sie die beiden Kapitel zu den regulären Ausdrücken durchgearbeitet haben, sollten Sie genug über reguläre Ausdrücke wissen, um so ziemlich jedes Muster mit einem beliebigen Satz an Daten zu vergleichen.

Fragen und Antworten

Frage:
Ich habe hier einige Codezeilen, die in zwei Schritten versuchen, etwas aus einem String herauszuziehen. Das erste Muster legt einen Teilstring in $1 ab, und dann durchsucht das zweite Muster $1 nach einem weiteren Muster. Das zweite Muster führt nie zu einer Übereinstimmung, und die Ausgabe von $1 zeigt, dass die Variable leer ist. Wenn diese Variable aber leer ist, so hätte das zweite Muster eigentlich gar nicht erst verglichen werden sollen. Was läuft hier falsch?

Antwort:
Das klingt, als wenn Sie ungefähr folgendes versucht hätten:

    if ($string =~ /ein langes Muster mit einem {Teilmuster} darin/) {
if ($1 =~ /ein zweites Muster/) {
# verarbeitet zweites Muster
}
}

Frage:
Ich habe einige Skripts gesehen, die eine $*-Variable auf 1 gesetzt haben, um einen Mustervergleich über mehrere Zeilen durchzuführen - wozu sie die Option / m verwenden. Was bedeutet $*, und kann ich es auch verwenden?

Antwort:
In früheren Versionen von Perl hat man $* gesetzt, um Perl anzuweisen, die Bedeutung von ^ und $ zu ändern. In aktuellen Perl-Versionen jedoch sollten Sie statt dessen die Option /m verwenden. $* wurde lediglich aus Gründen der Rückwärtskompatibilität beibehalten.

Workshop

Der Workshop enthält Quizfragen, die Ihnen helfen sollen, Ihr Wissen zu festigen, und Übungen, die Sie anregen sollen, das eben Gelernte umzusetzen und eigene Erfahrungen zu sammeln. Versuchen Sie, das Quiz und die Übungen zu beantworten und zu verstehen, bevor Sie zur Lektion des nächsten Tages übergehen.

Quiz

  1. Angenommen $_ enthält 123 kazoo kazoo 456. Wie lauten die Ergebnisse der folgenden Ausdrücke?
        @matches = /(\b[^\d]+)\b/g;
    @matches = /\b[^\d]+\b/;
    s/\d{3}/xxx/;
    s/\d{3}/xxx/g;
    $matches = s/\d{3}/xxx/g;
    if (/\d+(.*)\d+/) { print $1;}
    @matches = split(/z/);
    @matches = split(" ",$_ 3);
  2. Wie lautet die Regel für die Numerierung der Rückbezüge und der Übereinstimmungsvariablen?
  3. Wie lange bestehen die Werte der Übereinstimmungsvariablen?
  4. Wie können Sie das gierige Verhalten der Quantifizierer + und * unterbinden?
  5. Was bewirken die folgenden Optionen?

Übungen

  1. FEHLERSUCHE: Was ist an diesem Codefragment falsch?
        while (<>) {
    $input =~ /pat\s/path /;
    }
  2. FEHLERSUCHE: Was ist an diesem Codefragment falsch?
        @matches = /\b[^\d]+\b/;
  3. Schreiben Sie ein Skript, das doppelte Wörter (»das das« oder »ein ein«) in der Eingabe findet und diese durch ein Vorkommen desselben Wortes ersetzt. Suchen Sie dabei auch nach doppelten Wörtern über zwei Zeilen.
  4. Schreiben Sie ein Skript, das Akronyme in der Eingabe aufschlüsselt (ersetzen Sie zum Beispiel »HTML« durch »HTML (Hypertext Markup Language)«. Verwenden Sie die folgenden Akronyme und deren Ersetzungen:
        HTML (HyperText Markup Language)
      ICBM (InterContinental Ballistic Missile)
      EEPROM (Electrically-erasable programmable read-only memory)
      SCUBA (self-contained underwater breathing apparatus)
      FAQ (Frequently Asked Questions)
  5. Modifizieren Sie das Skript img.pl, so dass es Links anstelle von Grafiken herauszieht und protokolliert. TIP: Links haben ungefähr folgendes Format:
        <A HREF="url_des_Links">Text des Links</A>
  6. Links können folgende Attribute enthalten: NAME, REL, REV, TARGET und TITLE.
  7. Stellen Sie sicher, dass Sie sowohl den Inhalt des Link-Tags protokollieren als auch den Text zwischen den öffnenden und schließenden Tags.

Antworten

Hier die Antworten auf die Workshop-Fragen aus dem vorigen Abschnitt.

Antworten zum Quiz

  1. Die Antworten lauten
  1. Die Numerierung der Rückbezüge und der Übereinstimmungsvariablen basieren auf den öffnenden Klammern. Die Klammern eines Musters können ineinander verschachtelt werden.
  2. Übereinstimmungsvariablen sind extrem kurzlebig. Ihre Werte sind nur bis zum nächsten Mustervergleich oder bis zum Ende des Blocks gültig.
  3. Zwei Möglichkeiten: Am besten vermeiden Sie gierige Quantifizierer durch die Verwendung des Punktzeichens (.) oder durch Einsatz negierter Zeichenklassen. Der zweite Weg führt über die nichtgierigen Versionen der Quantifizierer (+? und *?).
  4. Die Antworten lauten:

Lösungen zu den Übungen

  1. Das Muster besteht aus zwei Teilen (ein Muster und eine Ersetzung) aber keinem führenden s. Die korrekte Version hierfür sieht folgendermaßen aus:
        while (<>) {
    $input =~ s/pat\s/path /;
    }
  2. Fangfrage! An diesem Code ist eigentlich nichts falsch, außer dass er wahrscheinlich nicht das macht, was Sie erwarten. Ein Muster in einem Listenkontext, der keine Teilmuster in Klammern aufweist, ergibt den Wert (1), wenn das Muster zu einer Übereinstimmung führt. Um eine Liste der Übereinstimmungen zu sichern, müssen Sie irgendwo im Muster Klammern setzen.
  3. Hier eine mögliche Lösung:
        #!/usr/bin/perl -w
    #
    # Sucht doppelte Wörter ohne Rücksicht auf Zeilenumbrüche
    # diese Version sucht Groß- und Kleinbuchstaben, aber berücksichtigt
    # keine Zeichensetzung oder mehr als zwei Vorkommen des gleichen Wortes.

    $/ = ""; # Absatzeingabe-Modus

    while (<>) {
    s/\b(\w+)\s+\1\b/$1/ig;
    print;
    }
  4. Hier eine mögliche Lösung:
        #!/usr/bin/perl -w

    %acs = (
    "HTML" => "HyperText Markup Language",
    "ICBM" => "InterContinental Ballistic Missile",
    "EEPROM" => "Electrically-erasable programmable read-only memory",
    "SCUBA" => "self-contained underwater breathing apparatus",
    "FAQ" => "Frequently Asked Questions",
    );

    while (<>) {
    foreach $key (keys %acs) {
    s/$key/$key ($acs{$key})/gi;
    }
    print;
    }
  5. Hier ist eine Lösung:
        #!/usr/bin/perl -w
    # sucht und extrahiert Links
    # Lässt Link-Text mit eingebettetem HTML unberücksichtigt

    $/ = ""; # Absatzeingabe-Modus
    $raw = ""; # rohe Attribute
    $linktext = ""; # Link-Text
    %atts = (); # Attribute

    while (<>) {
    while (/<A\s+([^>]+)>([^<]+)<\/A>/ig) {
    $raw = $1;
    $linktext = $2;
    $linktext =~ s/[\s]*\n/ /g;
    while ($raw =~ /([^\s=]+)\s*=\s*("([^"]+)"|[^\s]+\s*)/ig) {
    if (defined $3) {
    $atts{ uc($1) } = $3;
    } else { $atts{ uc($1)} = $2; }
    }
    print '-' x 15;
    print "\nLink-Text: $linktext\n";
    foreach $key ("HREF", "NAME", "TITLE",
    "REL", "REV", "TARGET") {
    if (exists($atts{$key})) {
    $atts{$key} =~ s/[\s]*\n/ /g;
    print " $key: $atts{$key}\n";
    }
    }
    %atts = ();
    }
    }


vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH